package com.yuki312.backoff.core;

import java.util.concurrent.TimeUnit;

import ohos.agp.render.render3d.BuildConfig;

public class Backoff {

    /**
     * Give up on retry
     */
    public static final long ABORT = 0;

    /**
     * Debug trace
     */
    static final boolean TRACE = BuildConfig.DEBUG;

    private final BackoffAlgorithm algorithm;
    private final int maxRetryCount;
    private final long maxElapsedTime; // milliseconds

    private int retryCount = 0;
    private long elapsedTime = 0L;

    private Backoff(BackoffAlgorithm algorithm, int maxRetryCount, long maxElapsedTime) {
        this.algorithm = algorithm;
        this.maxRetryCount = maxRetryCount;
        this.maxElapsedTime = maxElapsedTime;
    }

    /**
     * This function returns the interval until the next retry.
     *
     * @return interval until the next retry
     */
    public long interval() {
        retryCount++;
        if (retryCount > maxRetryCount) {
            return ABORT;
        }

        long next = algorithm.interval(retryCount, elapsedTime);
        if (next == ABORT) {
            return ABORT;
        }
        if (next < 0) {
            throw new IllegalStateException("Interval is invalid. Must be greater than 0.");
        }

        elapsedTime += next;
        if (elapsedTime > maxElapsedTime) {
            return ABORT;
        }

        return next;
    }

    /**
     * This function returns the number of retries.
     *
     * @return number of retries
     */
    public int getRetryCount() {
        return retryCount;
    }

    /**
     * This function returns the elapsed time in milliseconds.
     *
     * @return elapsed time (milliseconds)
     */
    public long getElapsedTime() {
        return elapsedTime;
    }

    public static class Builder {

        public static final int DEFAULT_MAX_RETRY_COUNT = 10;

        public static final long DEFAULT_MAX_ELAPSED_TIME = 60_000L;

        private int maxRetryCount = DEFAULT_MAX_RETRY_COUNT;
        private long maxElapsedTime = DEFAULT_MAX_ELAPSED_TIME;
        private BackoffAlgorithm algorithm = new ExponentialAlgorithm();

        /**
         * Set backoff algorithm.
         * You can choose from the following algorithms or you can set your own algorithm.
         *
         * <ul>
         * <li>{@link FixedIntervalAlgorithm}</li>
         * <li>{@link ExponentialAlgorithm}</li>
         * </ul>
         * <p>
         * or e.g.
         * <pre><code>
         *   public long interval(int retryCount, long elapsedTime) {
         *     2F.pow(retry - 1).toLong().times(1000L).coerceAtMost(5000L)
         *   }
         * </code></pre>
         * <p>
         * When you want to force stop retrying process, return ABORT.
         *
         * @param algorithm Algorithm for calculating the interval time until the next retry
         * @return Backoff builder
         * @see BackoffAlgorithm
         */
        public Builder setAlgorithm(BackoffAlgorithm algorithm) {
            this.algorithm = algorithm;
            return this;
        }

        /**
         * Set the maximum count of retry.
         * The retry is aborted when either the maximum count or the maximum elapsed time is satisfied.
         * <p>
         * The count must be greater or equal 0.
         *
         * @param count maximum count of retry
         * @return Backoff builder
         * @see #DEFAULT_MAX_RETRY_COUNT
         * @see #setUnlimitedRetryCount()
         */
        public Builder setMaxRetryCount(int count) {
            if (count < 0) {
                throw new IllegalArgumentException("MaxRetryCount is invalid. Must be greater or equal 0.");
            }

            maxRetryCount = count;
            return this;
        }

        public Builder setUnlimitedRetryCount() {
            setMaxRetryCount(Integer.MAX_VALUE);
            return this;
        }

        /**
         * Set the maximum elapsed time to retry milliseconds time.
         * The retry is aborted when either the maximum count or the maximum elapsed time is satisfied.
         * <p>
         * The elapsed time must be greater or equal 0.
         *
         * @param elapsedTime maximum elapsed time in milliseconds
         * @param unit        the units of time that {@code elapsedTime} is expressed in
         * @return Backoff builder
         * @see #DEFAULT_MAX_ELAPSED_TIME
         * @see #setUnlimitedElapsedTime()
         */
        public Builder setMaxElapsedTime(long elapsedTime, TimeUnit unit) {
            long t = unit.toMillis(elapsedTime);
            if (t < 0) {
                throw new IllegalArgumentException("MaxElapsedTime is invalid. Must be greater or equal 0");
            }

            maxElapsedTime = t;
            return this;
        }

        public Builder setUnlimitedElapsedTime() {
            setMaxElapsedTime(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
            return this;
        }

        public Backoff build() {
            return new Backoff(algorithm, maxRetryCount, maxElapsedTime);
        }
    }

    static void trace(long next, long low, long high) {
        System.out.println(next + " (" + low + ".." + high + ")");
    }
}
